﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Threading;

namespace SerialAssist
{
    class ESP8266
    {
        class Response
        {
            public OpCode OpCode { get; private set; }

            public bool Success { get; private set; }

            public int ErrorCode { get; private set; }

            public UInt32 Result { get; private set; }

            public byte[] Data { get; private set; }

            public static Response ReadResponse(SerialPort p)
            {
                int b=0;
                int timeout = 0;

                while(p.BytesToRead==0 && timeout<100)
                {
                    Thread.Sleep(10);
                    timeout++;
                }

                try
                {
                    b = p.ReadByte();
                }
                catch
                {
                    b = 0;
                }

                if (b != 0xC0)
                {
                    p.DiscardInBuffer();
                    Response res = new Response();
                    res.Success = false;
                    return res;
                    //throw new ApplicationException("Invalid packet start character:" + string.Join(",", data.Select(x => x.ToString("X2"))));
                }

                List<byte> result = new List<byte>();
                while ((b = p.ReadByte()) != 0xC0)
                {
                    if (b == 0xDB)
                    {
                        b = p.ReadByte();

                        if (b == 0xDC)
                        {
                            result.Add(0xC0);
                        }
                        else if (b == 0xDD)
                        {
                            result.Add(0xDB);
                        }
                        else
                        {
                            byte[] data = ReadAllBytes(p);
                            throw new ApplicationException("Invalid escape sequence:" + string.Join(",", data.Select(x => x.ToString("X2"))));
                        }
                    }
                    else
                    {
                        result.Add((byte)b);
                    }
                }

                int respLength = result.Count;
                using (BinaryReader br = new BinaryReader(new MemoryStream(result.ToArray())))
                {
                    Response resp = new Response();
                    if (br.ReadByte() != (byte)OpDir.Response)
                    {
                        throw new ApplicationException("Invalid packet direction:" + string.Join(",", result.Select(x => x.ToString("X2"))));
                    }

                    resp.OpCode = (OpCode)br.ReadByte();

                    UInt16 bodySize = br.ReadUInt16();
                    if ((bodySize + 8) != respLength)
                    {
                        throw new ApplicationException("Invalid packet size:" + string.Join(",", result.Select(x => x.ToString("X2"))));
                    }

                    resp.Result = br.ReadUInt32();

                    if (bodySize == 2)
                    {
                        resp.Success = (0 == br.ReadByte());
                        resp.ErrorCode = br.ReadByte();
                    }
                    else
                    {
                        throw new ApplicationException("Invalid body size:" + string.Join(",", result.Select(x => x.ToString("X2"))));
                        //resp.Data = br.ReadBytes(bodySize);
                    }

                    return resp;
                }
            }
        }

        class Request
        {
            MemoryStream _ms = new MemoryStream();
            OpCode _op = OpCode.None;
            int _bodySize = 0;

            public Request(OpCode op)
            {
                _op = op;
            }

            public void WritePayload(byte[] payload, int offset, int length)
            {
                _bodySize += length;
                _ms.Write(payload, offset, length);
            }

            public void WritePayload(byte[] payload)
            {
                WritePayload(payload, 0, payload.Length);
            }

            public void WritePayload(UInt32 payload)
            {
                byte[] data = BitConverter.GetBytes(payload);
                WritePayload(data);
            }

            public void WritePayload(UInt16 payload)
            {
                byte[] data = BitConverter.GetBytes(payload);
                WritePayload(data);
            }

            public void WritePayload(byte payload)
            {
                _bodySize++;
                _ms.WriteByte(payload);
            }

            public Response DoRequest(SerialPort p, UInt32 delay)
            {
                byte[] data = new byte[8 + _bodySize];
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(data)))
                {
                    bw.Write((byte)OpDir.Request);
                    bw.Write((byte)_op);
                    bw.Write((UInt16)_bodySize);

                    _ms.Flush();
                    byte[] payload = _ms.GetBuffer();

                    if (_op == OpCode.FlashDl_Data || _op == OpCode.RamDl_Data)
                    {
                        byte checksum = CheckSum(payload, sizeof(UInt32) * 4, (int)_ms.Length, CHECKSUM_MAGIC);
                        bw.Write((UInt32)checksum);
                    }
                    else
                    {
                        bw.Write((UInt32)0);
                    }

                    bw.Write(payload, 0, (int)_ms.Length);
                }

                data = ToPacket(data);

                p.DiscardInBuffer();

                Response res = new Response();

                try
                {
                    p.Write(data, 0, data.Length);
                    res = Response.ReadResponse(p);
                }
                catch
                {

                }
                

                

                return res;
            }
        }

        public enum Esp8266FlashStates
        {
            Start,
            Connecting,
            Erasing,
            Programming,
            Failed,
        }

       

        enum OpCode
        {
            FlashDl_Start = 0x02,
            FlashDl_Data = 0x03,
            FlashDl_Finish = 0x04,
            RamDl_Start = 0x05,
            RamDl_Finish = 0x06,
            RamDl_Data = 0x07,
            SyncFrame = 0x08,
            Write_Reg = 0x09,
            Read_Reg = 0x0A,
            Config_SPI = 0x0B,
            None,
        }

        enum OpDir
        {
            Request = 0x00,
            Response = 0x01,
        }

        const byte CHECKSUM_MAGIC = 0xEF;

        bool _Terminate = false;


        public delegate void Esp8266FlashStateChangedHandler(Esp8266FlashStates state, object info);
        public event Esp8266FlashStateChangedHandler Esp8266FlashStateChanged;
        protected void RiseEsp8266FlashStateChanged(Esp8266FlashStates state, object info)
        {
            if (Esp8266FlashStateChanged != null)
            {
                Esp8266FlashStateChanged(state, info);
            }
        }

        static byte CheckSum(byte[] data, int offset, int length, byte checksum)
        {
            byte result = checksum;
            for (int i = offset; i < length; i++)
            {
                result ^= data[i];
            }

            return result;
        }

        static byte[] ReadAllBytes(SerialPort p)
        {
            byte[] data = new byte[p.BytesToRead];
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = (byte)p.ReadByte();
            }

            return data;
        }

        

        SerialPort _port;

        static byte[] ToPacket(byte[] data)
        {
            List<byte> result = new List<byte>();
            result.Add(0xC0);
            foreach (var b in data)
            {
                if (b == 0xC0)
                {
                    result.Add(0xDB);
                    result.Add(0xDC);
                }
                else if (b == 0xDB)
                {
                    result.Add(0xDB);
                    result.Add(0xDD);
                }
                else
                {
                    result.Add(b);
                }
            }
            result.Add(0xC0);

            return result.ToArray();
        }


        const UInt32 SYNC_MAGIC = 0x20120707;
        const byte SYNC_DATA = 0x55;
        const int SYNC_DATA_SIZE = 32;

        bool Sync()
        {
            Request req = new Request(OpCode.SyncFrame);

            req.WritePayload(SYNC_MAGIC);
            req.WritePayload(Enumerable.Repeat<byte>(SYNC_DATA, SYNC_DATA_SIZE).ToArray());

            Response resp = new Response();
            int tried = 0;
            bool Success;
            do
            {
                resp = req.DoRequest(_port, 0);
                tried++;
                Success = resp.Success;
            } while (Success == false && tried < 10 && _Terminate==false);

            Thread.Sleep(1000);

            _port.DiscardInBuffer();

            return resp.Result == SYNC_MAGIC;
        }

        public bool Connect(SerialPort sp)
        {
            RiseEsp8266FlashStateChanged(Esp8266FlashStates.Connecting, string.Empty);

            _port = sp;
            _port.ReadTimeout = 5000;

            _port.DtrEnable = false;
            _port.RtsEnable = true;
            Thread.Sleep(10);

            _port.DtrEnable = true;
            _port.RtsEnable = false;
            Thread.Sleep(15);

            _Terminate = false;

            for (int i = 1; i <= 10 && _Terminate==false; i++)
            {
                RiseEsp8266FlashStateChanged(Esp8266FlashStates.Connecting, i);
                try
                {
                    if (Sync())
                    {
                        //_port.ReadTimeout = oldTimeout;
                        return true;
                    }
                }
                catch (System.TimeoutException)
                {

                }               
            }

            _port.Close();
            _port = null;

            return false;
        }

        public UInt32 ReadRegister(UInt32 Address)
        {
            Request req = new Request(OpCode.Read_Reg);

            req.WritePayload(Address);

            Response resp = req.DoRequest(_port, 0);
            return resp.Result;
        }

        //OTP ROM addresses
        const UInt32 ESP_OTP_MAC0 = 0x3ff00050;
        const UInt32 ESP_OTP_MAC1 = 0x3ff00054;
        const UInt32 ESP_OTP_MAC2 = 0x3ff00058;
        const UInt32 ESP_OTP_MAC3 = 0x3ff0005C;

        public string ReadMacAddr()
        {
            UInt32 mac1 = ReadRegister(ESP_OTP_MAC0);
            UInt32 mac2 = ReadRegister(ESP_OTP_MAC1);
            UInt32 mac3 = ReadRegister(ESP_OTP_MAC2);
            UInt32 mac4 = ReadRegister(ESP_OTP_MAC3);

            return string.Format("18:FE:34:{0:X2}:{1:X2}:{2:X2}", (mac2 >> 8) & 0xff, mac2 & 0xff, (mac1 >> 24) & 0xff);
        }

        public bool FlashDownloadStart(UInt32 offset, UInt32 totalSize, UInt32 blockSize)
        {
            Request req = new Request(OpCode.FlashDl_Start);

            req.WritePayload(totalSize);
            req.WritePayload((totalSize + blockSize - 1) / blockSize);
            req.WritePayload(blockSize);
            req.WritePayload(offset);

            Response resp = req.DoRequest(_port, 0);
            return resp.Success;
        }

        public bool FlashDownloadData(UInt32 sequence, byte[] data, int offset, int length, int blockSize)
        {
            if (offset < 0 || offset + length > data.Length || length > blockSize)
            {
                throw new ArgumentException();
            }

            Request req = new Request(OpCode.FlashDl_Data);

            req.WritePayload((UInt32)blockSize);
            req.WritePayload(sequence);
            req.WritePayload((UInt32)0);
            req.WritePayload((UInt32)0);
            req.WritePayload(data, offset, length);

            if (length < blockSize)
            {
                req.WritePayload(Enumerable.Repeat<byte>(0xE0, blockSize - length).ToArray());
            }

            Response resp = req.DoRequest(_port, 0);
            return resp.Success;
        }

        public bool FlashDownloadFinish(bool reboot)
        {
            Request req = new Request(OpCode.FlashDl_Finish);

            req.WritePayload(reboot ? (UInt32)1 : (UInt32)0);

            Response resp = req.DoRequest(_port, 0);
            return resp.Success;
        }

        const UInt32 FLASH_BLOCK_SIZE = 0x100;
        public bool FlashDownload(byte[] image, UInt32 offset, bool reboot)
        {
            if (_Terminate)
                return false;

            //Console.WriteLine("Erasing...");
            RiseEsp8266FlashStateChanged(Esp8266FlashStates.Erasing, "正在擦除");

            if (!FlashDownloadStart(offset, (UInt32)image.Length, FLASH_BLOCK_SIZE))
            {
                return false;
            }

            UInt32 sequence = 0;
            for (int i = 0; i < image.Length && _Terminate == false; i += (int)FLASH_BLOCK_SIZE)
            {
                int length = image.Length - i;

                if (length > FLASH_BLOCK_SIZE)
                {
                    length = (int)FLASH_BLOCK_SIZE;
                }

                //Console.WriteLine("Programming {0:X8}...", offset + sequence * FLASH_BLOCK_SIZE);
                RiseEsp8266FlashStateChanged(Esp8266FlashStates.Programming, i * 100 / image.Length);

                try
                {
                    if (!FlashDownloadData(sequence, image, i, length, (int)FLASH_BLOCK_SIZE))
                    {
                        throw new ApplicationException("Flash download failed");
                    }
                }
                catch (Exception e)
                {
                    //Console.WriteLine("Failed");
                    RiseEsp8266FlashStateChanged(Esp8266FlashStates.Failed, e.Message);
                    //Console.WriteLine(e.Message);
                }

                sequence++;
            }

            //return FlashDownloadFinish(reboot);
            return true;
        }

        public void Terminate()
        {
            _Terminate = true;
        }
    }
}
